Skip to content

Accounts Refactor PR 1: Adding account -> [credential_id] mapping#2770

Open
citizen-stig wants to merge 7 commits intotheodore/multisig-upgradefrom
nikolai/accounts-refactor-part-1
Open

Accounts Refactor PR 1: Adding account -> [credential_id] mapping#2770
citizen-stig wants to merge 7 commits intotheodore/multisig-upgradefrom
nikolai/accounts-refactor-part-1

Conversation

@citizen-stig
Copy link
Copy Markdown
Member

@citizen-stig citizen-stig commented Apr 21, 2026

Description

PR 1 of the accounts refactor series. Introduces a new account_owners authorization set alongside the existing accounts map and stops writing to the legacy map from every code path. The legacy map remains readable so pre-upgrade state keeps routing.

What changed

  • New state: account_owners: StateMap<(address, credential_id), bool>.
  • InsertCredentialId now writes account_owners, not accounts.
  • Genesis writes authorizations to account_owners.
  • resolve_sender_address becomes read-only (no auto-create on miss).
  • New capabilities: is_authorized(addr, cred) and is_authorized_for(addr, cred).
  • accounts map kept purely as a read-only legacy fallback. No new writers.
  • hyperlane-solana-register migrated from resolve_sender_address to is_authorized_for.
  • Map uses boolean instead of unit type () because of limitations of rockbound: Refactoring for more explicit sentinel values handling rockbound#50

Trade-offs / behavioral deltas

  • InsertCredentialId(X) from sender A no longer makes X route to A. It only authorizes X for A in account_owners. When X later signs a V0 tx, the resolver returns X.into() (stateless default), unless a pre-upgrade entry exists in legacy accounts. Fund the canonical address (or rely on a pre-upgrade entry) if you need X to draw gas from A.
  • The "no hijacking" guard is narrower. exit_if_credential_exists now checks (sender, credential) in account_owners instead of global credential uniqueness in accounts. Authorizing someone else's credential for your own address now succeeds — harmless, since they still need the private key to sign.
  • get_account(credential_id) returns AccountEmpty for post-PR credentials. New writes don't land in accounts. A get_addresses_for_credential query for the new model ships in a follow-up; tooling owners should plan accordingly.
  • is_authorized_for gives legacy mappings exclusive priority. If a credential has a legacy accounts entry, only that address is authorized (canonical match and account_owners are not consulted). Doc updated to reflect this; code relies on the invariant that no code path writes both maps for the same credential.

Backward compatibility

  • On-chain state: pre-upgrade accounts entries keep routing until explicitly migrated.
  • Wire: AccountConfig, CallMessage, and tx schema unchanged.
  • Client-visible regression: get_account (see above).

Known gaps / deferred

  • DrainLegacyAccounts migration and get_addresses_for_credential query land in a later PR.
  • resolve_sender_address is now pure-read but still takes &mut self and StateAccessor, duplicating resolve_sender_address_read_only. Two callers in sov-capabilities need migration before the mutable variant can be deleted — deferred.
  • is_authorized_for legacy-first precedence is not enforced in code; relies on "no writer touches both maps for the same credential". Fine today, brittle if a future migration ever violates it.

  • Will be done when merging to dev I have updated CHANGELOG.md with a new entry if my PR makes any breaking changes or fixes a bug. If my PR removes a feature or changes its behavior, I provide help for users on how to migrate to the new behavior.
  • I have carefully reviewed all my Cargo.toml changes before opening the PRs. (Are all new dependencies necessary? Is any module dependency leaked into the full-node (hint: it shouldn't)?)

Linked Issues

Testing

New tests added in sov-accounts integration tests (test_setup_multisig_and_act, test_resolve_address_with_multi_credential_ownership, etc.). Existing test_multisig_signature_verification in auth_eip712 updated to seed the multisig's canonical address at genesis under the new model.

Docs

sov-accounts/README.md expanded with the three-relation model (legacy mapping / stateless canonical / explicit authorization). Function docstrings updated.

@citizen-stig citizen-stig changed the title PR 1: Adding account -> [credential_id] mapping Accounts Refactor PR 1: Adding account -> [credential_id] mapping Apr 22, 2026
Comment thread crates/module-system/module-implementations/sov-accounts/src/call.rs Outdated
Comment thread crates/module-system/module-implementations/sov-accounts/src/call.rs Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

.accounts
.resolve_sender_address(&address, &credential_id, state)
.map_err(CoreModuleError::state_write)?;
.is_authorized_for(&address, &credential_id, state)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_sender_address was being used for the side-effect of the credential id being registered for the address, it looks like that's no longer the case here. How would the credential id be registered now?

// the payer address, so `is_authorized_for` succeeds via the canonical-address
// arm without needing any stored `accounts` or `account_owners` entry. This
// exercises the stateless happy path.
let embedded = payer;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be changed, the payer and embedded should explicitly be different

Explains a bit more about what the module is meant to achieve: https://github.com/Sovereign-Labs/sovereign-sdk/tree/dev/crates/module-system/module-implementations/extern/hyperlane-solana-register#message-structure

Namely:

The embedded public key becomes the CredentialId on the rollup, and it's associated with the rollup address derived from the payer's public key.
The result of this process is that the embedded wallet controls the users Solana address on the rollup. This is desirable in situations like Zetas where we want the embedded wallet to be opaque, with the user feeling like they're using their Solana wallet directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants